package com.elitel.common.utils;

import com.elitel.base.entity.sqlparse.InputParam;
import org.apache.ibatis.builder.ParameterExpression;
import org.apache.ibatis.builder.StaticSqlSource;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.parsing.GenericTokenParser;
import org.apache.ibatis.parsing.TokenHandler;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.scripting.defaults.RawSqlSource;
import org.apache.ibatis.scripting.xmltags.*;
import org.apache.ibatis.session.Configuration;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import static com.elitel.base.entity.sqlparse.ParamExpType.jdbcType;
import static com.elitel.base.entity.sqlparse.ParamExpType.property;
import static com.elitel.base.entity.sqlparse.ParamType.*;
import static java.util.stream.Collectors.toList;

/**
 * @author xqh
 * @Date 2024/9/23
 * @description: 参数解析
 */
public class ParamUtils {

    /**
     * 参数类型映射表
     * key: SQL唯一键，value:  Md5(sql)
     */
    public static final ConcurrentHashMap<String, String> paramTypeMap = new ConcurrentHashMap<>();

    private static final String verifyTemplate = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
            + "<!DOCTYPE mapper\n"
            + "        PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\"\n"
            + "        \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n"
            + "<mapper namespace=\"Verify\">\n"
            + "    <select id=\"verify\">\n"
            + "    %s\n"
            + "    </select>\n"
            + "</mapper>";

    public static SqlSource xmlVerify(String xml) {
        xml = String.format(verifyTemplate, xml);
        InputStream inputStream = new ByteArrayInputStream(xml.getBytes());
        Configuration configuration = new Configuration();
        Map<String, XNode> sqlFragments = configuration.getSqlFragments();
        XMLMapperBuilder xmlMapperBuilder =
                new XMLMapperBuilder(inputStream, configuration, "Verify", sqlFragments);

        xmlMapperBuilder.parse();
        MappedStatement mappedStatement = configuration.getMappedStatement("verify", false);
        return mappedStatement.getSqlSource();
    }

    public static List<InputParam> parseInputParam(String xml) {
        SqlSource sqlSource = xmlVerify(xml);
        return parseInputParam(sqlSource);
    }

    public static List<InputParam> parseInputParam(SqlSource sqlSource) {
        if (sqlSource instanceof RawSqlSource) {
            return parseRawParam((RawSqlSource) sqlSource);
        }
        if (sqlSource instanceof DynamicSqlSource) {
            return parseDynamicParam((DynamicSqlSource) sqlSource);
        }
        return Collections.emptyList();
    }

    private static List<InputParam> parseDynamicParam(DynamicSqlSource sqlSource) {
        SqlNode sqlNode = getFieldValue(sqlSource, "rootSqlNode");
        return parseSqlNode(sqlNode, true);
    }

    private static List<InputParam> parseSqlNode(SqlNode sqlNode, boolean required) {
        if (sqlNode instanceof MixedSqlNode) {
            return parseMixedSqlNodeParam((MixedSqlNode) sqlNode, required);
        }
        if (sqlNode instanceof TextSqlNode) {
            return parseTextSqlNodeParam((TextSqlNode) sqlNode, required);
        }
        if (sqlNode instanceof StaticTextSqlNode) {
            return parseStaticTextSqlNodeParam((StaticTextSqlNode) sqlNode, required);
        }
        if (sqlNode instanceof IfSqlNode) {
            return parseIfSqlNodeParam((IfSqlNode) sqlNode);
        }
        if (sqlNode instanceof ForEachSqlNode) {
            return parseForEachSqlNodeParam((ForEachSqlNode) sqlNode, required);
        }
        if (sqlNode instanceof ChooseSqlNode) {
            return parseChooseSqlNodeParam((ChooseSqlNode) sqlNode);
        }
        if (sqlNode instanceof TrimSqlNode) {
            return parseTrimSqlNodeParam((TrimSqlNode) sqlNode, required);
        }
        if (sqlNode instanceof VarDeclSqlNode) {
            return parseVarDeclSqlNodeParam((VarDeclSqlNode) sqlNode, required);
        }
        return Collections.emptyList();
    }

    private static List<InputParam> parseVarDeclSqlNodeParam(VarDeclSqlNode sqlNode, boolean required) {
        String name = getFieldValue(sqlNode, "name");
        String expression = getFieldValue(sqlNode, "expression");
        return Collections.singletonList(new InputParam(name, BIND, expression, required));
    }

    private static List<InputParam> parseTrimSqlNodeParam(TrimSqlNode sqlNode, boolean required) {
        SqlNode contents = getFieldValue(sqlNode, "contents");
        return parseSqlNode(contents, required);
    }

    /**
     * 解析ChooseSqlNode节点的参数
     * 该方法主要用于解析动态SQL节点ChooseSqlNode中的参数
     * 它会递归地解析ifSqlNodes和defaultSqlNode中的所有参数并将它们添加到参数列表中
     *
     * @param sqlNode ChooseSqlNode对象，包含了ifSqlNodes和defaultSqlNode
     * @return 返回一个包含所有解析出的输入参数的列表
     */
    private static List<InputParam> parseChooseSqlNodeParam(ChooseSqlNode sqlNode) {
        List<InputParam> chooseParamList = new ArrayList<InputParam>();

        List<SqlNode> ifSqlNodes = getFieldValue(sqlNode, "ifSqlNodes");
        ifSqlNodes.forEach(content -> chooseParamList.addAll(parseSqlNode(content, false)));

        SqlNode defaultSqlNode = getFieldValue(sqlNode, "defaultSqlNode");
        if (defaultSqlNode != null) {
            chooseParamList.addAll(parseSqlNode(defaultSqlNode, false));
        }
        return chooseParamList;
    }

    /**
     * 解析 ForEachSqlNode 的参数列表
     * <p>
     * 对于每个 ForEachSqlNode，提取其 collectionExpression 字段值，并将其作为参数添加到列表中
     * 这个方法专注于处理对于每个 SQL 节点迭代的参数，通常用于动态 SQL 生成或预编译过程中
     *
     * @param sqlNode  SQL 节点，预期为 ForEachSqlNode 类型
     * @param required 标志位，指示参数是否为必填
     * @return 返回包含 InputParam 对象的列表，其中每个 InputParam 对象代表 ForEachSqlNode 的一个参数
     */
    private static List<InputParam> parseForEachSqlNodeParam(ForEachSqlNode sqlNode, boolean required) {
        List<InputParam> forEachParamList = new ArrayList<InputParam>();
        String collectionExpression = getFieldValue(sqlNode, "collectionExpression");
        //todo 没有.的情况要兼容
        if (collectionExpression.contains("."))collectionExpression = collectionExpression.substring(0, collectionExpression.indexOf("."));
        String item = getFieldValue(sqlNode, "item");
        if (item != null) {
            SqlNode forEachSqlNode = getFieldValue(sqlNode, "contents");
            List<InputParam> paramList = parseSqlNode(forEachSqlNode, false);
            String finalCollectionExpression = collectionExpression;
            paramList.stream()
                    .filter(param -> param.getName() != null && param.getName().equals(item))
                    .findFirst()
                    .ifPresent(inputParam -> forEachParamList.add(
                            new InputParam(finalCollectionExpression, PRE_COMPILE, null, required)
                    ));

        } else {
            forEachParamList.add(new InputParam(collectionExpression, PRE_COMPILE, null, required));
        }


        return forEachParamList;
    }

    private static List<InputParam> parseIfSqlNodeParam(IfSqlNode sqlNode) {
        SqlNode contents = getFieldValue(sqlNode, "contents");
        return parseSqlNode(contents, false);
    }

    private static List<InputParam> parseStaticTextSqlNodeParam(StaticTextSqlNode sqlNode, boolean required) {
        TextSqlNodeTokenHandler handler = new TextSqlNodeTokenHandler();
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        parser.parse(getFieldValue(sqlNode, "text"));

        return handler.getParamSet().stream()
                .map(param -> {
                    ParameterExpression parameterExpression = new ParameterExpression(param);
                    return new InputParam(parameterExpression.get(property.name()), PRE_COMPILE, null, required,
                            parameterExpression.get(jdbcType.name()));
                })
                .collect(toList());
    }

    private static List<InputParam> parseTextSqlNodeParam(TextSqlNode sqlNode, boolean required) {
        TextSqlNodeTokenHandler handler1 = new TextSqlNodeTokenHandler();
        GenericTokenParser parser1 = new GenericTokenParser("${", "}", handler1);
        parser1.parse(getFieldValue(sqlNode, "text"));

        TextSqlNodeTokenHandler handler2 = new TextSqlNodeTokenHandler();
        GenericTokenParser parser2 = new GenericTokenParser("#{", "}", handler2);
        parser2.parse(getFieldValue(sqlNode, "text"));
        List<InputParam> all = new ArrayList<>();
        all.addAll(handler1.getParamSet().stream()
                .map(param -> {
                    ParameterExpression parameterExpression = new ParameterExpression(param);
                    return new InputParam(
                            parameterExpression.get(property.name()),
                            REPLACE,
                            null,
                            required,
                            parameterExpression.get(jdbcType.name())
                    );
                })
                .collect(toList()));
        all.addAll(handler2.getParamSet().stream()
                .map(param -> {
                    ParameterExpression parameterExpression = new ParameterExpression(param);
                    return new InputParam(
                            parameterExpression.get(property.name()),
                            PRE_COMPILE,
                            null,
                            required,
                            parameterExpression.get(jdbcType.name())
                    );
                })
                .collect(toList()));
        return all;
    }


    private static List<InputParam> parseMixedSqlNodeParam(MixedSqlNode sqlNode, boolean required) {
        List<SqlNode> contents = getFieldValue(sqlNode, "contents");
        return contents.stream()
                .map(node -> parseSqlNode(node, required))
                .flatMap(Collection::stream)
                .collect(toList());
    }

    private static List<InputParam> parseRawParam(RawSqlSource sqlSource) {
        StaticSqlSource SqlSource = getFieldValue(sqlSource, "sqlSource");
        List<ParameterMapping> parameterMappings = getFieldValue(SqlSource, "parameterMappings");
        return parameterMappings.stream()
                .map(parameterMapping ->
                        new InputParam(parameterMapping.getProperty(), PRE_COMPILE, null, true,
                                Optional.ofNullable(parameterMapping.getJdbcType()).map(Enum::name).orElse("")))
                .collect(toList());
    }

    private static <T> T getFieldValue(Object o, String fieldName) {
        return getFieldValue(o.getClass(), o, fieldName);
    }

    private static <T> T getFieldValue(Class clazz, Object o, String fieldName) {
        try {
            Field field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
            return (T) field.get(o);
        } catch (Exception e) {
            if (clazz.getSuperclass() != null) {
                return getFieldValue(clazz.getSuperclass(), o, fieldName);
            } else {
                return null;
            }
        }
    }

    static class TextSqlNodeTokenHandler implements TokenHandler {

        private final Set<String> paramSet = new HashSet<String>();

        public Set<String> getParamSet() {
            return paramSet;
        }

        @Override
        public String handleToken(String content) {
            paramSet.add(content);
            return content;
        }
    }


}